﻿using System;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using DirectShowLib;
using System.Runtime.InteropServices;
using System.IO;

namespace SimpleMediaManager.Controls
{
    public enum PlayState
    {
        Closed,
        Open,
        Playing,
        Paused,
        Stopped
    };

    public enum MediaType
    {
        Unknown,
        Audio,
        AudioVideo
    }

    public partial class VideoPlayback : UserControl
    {
        private const int WM_GRAPH_NOTIFY = 0x0400 + 13;
        private const int VOLUME_MAX = 0;
        private const int VOLUME_MIN = -10000;

        private IGraphBuilder graphBuilder;
        private IMediaControl mediaControl;
        private IMediaEventEx mediaEventEx;
        private IVideoWindow videoWindow;
        private IBasicAudio basicAudio;
        private IBasicVideo basicVideo;
        private IMediaSeeking mediaSeeking;
        private IMediaPosition mediaPosition;
        private IVideoFrameStep frameStep;
        private IntPtr drain = IntPtr.Zero;

        public string MediaFile { get; internal set; }
        public MediaType MediaType { get; internal set; }
        public PlayState PlaybackStatus { get; internal set; }
        public TimeSpan? Length { get; internal set; }
        public Size? Resolution { get; internal set; }

        // TODO: Remove
        private int currentVolume = VOLUME_MAX;

        public VideoPlayback()
        {
            InitializeComponent();
        }

        private void VideoPlayback_Load(object sender, EventArgs e)
        {
            this.MediaFile = null;
            this.MediaType = MediaType.Unknown;
            this.PlaybackStatus = PlayState.Closed;
            this.Length = null;
            this.Resolution = null;

            this.PlaybackRate = 1.0;
            this.Volume = 100;
            this.Mute = false;
            this.FullScreen = false;

            // TODO: Event?
        }

        public double? PlaybackPosition
        {
            get
            {
                double position;
                this.mediaPosition.get_CurrentPosition(out position);
                return position;
            }
        }
        public double PlaybackRate { get; set; }
        public int Volume { get; set; }
        public bool Mute { set; get; }
        public bool FullScreen { set; get; }

        public void OpenFile(string filename)
        {
            // Check filename
            if (filename == string.Empty)
            {
                throw new ArgumentNullException();
            }

            // Check file exists
            if (File.Exists(filename))
            {
                this.MediaFile = filename;
            }
            else
            {
                throw new ArgumentException("File does not exist");
            }

            int hr = 0;

            // Create graph
            this.graphBuilder = (IGraphBuilder)new FilterGraph();

            // Open file
            hr = this.graphBuilder.RenderFile(this.MediaFile, null);
            DsError.ThrowExceptionForHR(hr);

            this.mediaControl = (IMediaControl)this.graphBuilder;
            this.mediaEventEx = (IMediaEventEx)this.graphBuilder;
            this.mediaSeeking = (IMediaSeeking)this.graphBuilder;
            this.mediaPosition = (IMediaPosition)this.graphBuilder;
            this.videoWindow = this.graphBuilder as IVideoWindow;
            this.basicVideo = this.graphBuilder as IBasicVideo;
            this.basicAudio = this.graphBuilder as IBasicAudio;

            // Detect media type
            this.MediaType = DetectMediaType();

            // Detect video resolution
            this.Resolution = DetectVideoResolution();

            // Detect media duration
            int seconds = DetectMediaLength();
            this.Length = new TimeSpan(0, 0, 0, seconds, 0);

            //AspectRatio(1);

            ScaleVideoFit();

            this.PlaybackStatus = PlayState.Open;
        }

        public void CloseFile()
        {
            int hr = 0;

            // Stop media playback
            if (this.mediaControl != null)
            {
                hr = this.mediaControl.Stop();
            }

            // Free DirectShow interfaces
            try
            {
                lock (this)
                {
                    // Relinquish ownership after hiding video window
                    if (this.MediaType == MediaType.AudioVideo)
                    {
                        hr = this.videoWindow.put_Visible(OABool.False);
                        DsError.ThrowExceptionForHR(hr);

                        hr = this.videoWindow.put_Owner(IntPtr.Zero);
                        DsError.ThrowExceptionForHR(hr);
                    }

                    if (this.mediaEventEx != null)
                    {
                        hr = this.mediaEventEx.SetNotifyWindow(IntPtr.Zero, 0, IntPtr.Zero);
                        DsError.ThrowExceptionForHR(hr);
                    }

                    // Release and zero DirectShow interfaces
                    if (this.mediaEventEx != null) this.mediaEventEx = null;
                    if (this.mediaSeeking != null) this.mediaSeeking = null;
                    if (this.mediaPosition != null) this.mediaPosition = null;
                    if (this.mediaControl != null) this.mediaControl = null;
                    if (this.basicAudio != null) this.basicAudio = null;
                    if (this.basicVideo != null) this.basicVideo = null;
                    if (this.videoWindow != null) this.videoWindow = null;
                    if (this.frameStep != null) this.frameStep = null;

                    if (this.graphBuilder != null)
                    {
                        Marshal.ReleaseComObject(this.graphBuilder);
                        this.graphBuilder = null;
                    }

                    GC.Collect();
                }
            }
            catch { }

            // Reset properties
            this.MediaFile = null;
            this.MediaType = MediaType.Unknown;
            this.Length = null;
            this.Resolution = null;
            this.PlaybackStatus = PlayState.Closed;
        }

        public void Play()
        {
            int hr = 0;

            // Have the graph signal event via window callbacks for performance
            hr = this.mediaEventEx.SetNotifyWindow(this.Handle, WM_GRAPH_NOTIFY, IntPtr.Zero);
            DsError.ThrowExceptionForHR(hr);

            if (this.MediaType == MediaType.AudioVideo)
            {
                // Setup the video window
                hr = this.videoWindow.put_Owner(this.Handle);
                DsError.ThrowExceptionForHR(hr);

                hr = this.videoWindow.put_WindowStyle(WindowStyle.Child | WindowStyle.ClipSiblings | WindowStyle.ClipChildren);
                DsError.ThrowExceptionForHR(hr);

                // Set video scale
                //ScaleVideo(1);
            }

            // Run the graph to play the media file
            hr = this.mediaControl.Run();
            DsError.ThrowExceptionForHR(hr);

            this.PlaybackStatus = PlayState.Playing;
        }

        public void Stop()
        {
            int hr = 0;
            DsLong pos = new DsLong(0);

            if ((this.mediaControl == null) || (this.mediaSeeking == null)) return;

            // Stop and reset postion to beginning
            if ((this.PlaybackStatus == PlayState.Paused) || (this.PlaybackStatus == PlayState.Playing))
            {
                hr = this.mediaControl.Stop();

                this.PlaybackStatus = PlayState.Stopped;

                // Seek to the beginning
                hr = this.mediaSeeking.SetPositions(pos, AMSeekingSeekingFlags.AbsolutePositioning, null, AMSeekingSeekingFlags.NoPositioning);

                // Display the first frame to indicate the reset condition
                hr = this.mediaControl.Pause();
            }
        }

        public void TogglePause()
        {
            if (this.mediaControl == null) return;

            if (this.PlaybackStatus == PlayState.Paused)
            {
                if (this.mediaControl.Run() >= 0) this.PlaybackStatus = PlayState.Playing;
            }
            else if (this.PlaybackStatus == PlayState.Playing)
            {
                if (this.mediaControl.Pause() >= 0) this.PlaybackStatus = PlayState.Paused;
            }
        }

        public void Pause()
        {
            if (this.mediaControl.Pause() >= 0) this.PlaybackStatus = PlayState.Paused;
        }

        private MediaType DetectMediaType()
        {
            int hr = 0;
            OABool visible;
            MediaType detected;

            if ((this.videoWindow == null) || (this.basicVideo == null))
            {
                // Audio only or unknown video codec
                return MediaType.Audio;
            }
            else
            {
                // May have found video
                detected = MediaType.AudioVideo;
            }

            hr = this.videoWindow.get_Visible(out visible);

            if (hr < 0)
            {
                // Will fail if audio only
                // If the video codec is unsuported, may still get audio
                if (hr == unchecked((int)0x80004002)) //E_NOINTERFACE
                {
                    return MediaType.Audio;
                }
                else
                {
                    DsError.ThrowExceptionForHR(hr);
                }
            }

            return detected;
        }

        private Size? DetectVideoResolution()
        {
            int hr = 0;
            int height, width;

            if (this.basicVideo == null || this.MediaType != MediaType.AudioVideo) return null;

            // Read the default video size
            hr = this.basicVideo.GetVideoSize(out width, out height);

            if (hr == DsResults.E_NoInterface) return null;

            return new Size(width, height);
        }

        private int DetectMediaLength()
        {
            int hr = 0;
            long duration;

            this.mediaSeeking.GetDuration(out duration);
            DsError.ThrowExceptionForHR(hr);

            // Duration is in 100-nanosecond units, convert to seconds
            return Convert.ToInt32(duration / 10000000);
        }

        private void ScaleVideo(int multiplier)
        {
            // Check there is video
            if (this.Resolution == null) return;

            int hr = this.videoWindow.SetWindowPosition(0, 0, this.Resolution.Value.Width * multiplier, this.Resolution.Value.Height * multiplier);
            DsError.ThrowExceptionForHR(hr);
        }

        private void ScaleVideoExact(int width, int height)
        {
            if (this.Resolution == null) return;

            int hr = this.videoWindow.SetWindowPosition(0, 0, width, height);
            DsError.ThrowExceptionForHR(hr);
        }

        public void ScaleVideoFit()
        {
            if (this.Resolution == null) return;

            double videoRes = (double)this.Resolution.Value.Width / (double)this.Resolution.Value.Height;
            double frameRes = (double)this.Width / (double)this.Height;

            int width = 0;
            int height = 0;
            int top = 0;
            int left = 0;
            int hr = 0;

            if (Program.Settings.VideoScale)
            {
                this.Parent.MinimumSize = new System.Drawing.Size(100, 50);
                if (videoRes > frameRes)
                {
                    width = this.Width;
                    height = (int)Math.Floor(width / videoRes);
                    top = (this.Height - height) / 2;
                }
                else
                {
                    height = this.Height;
                    width = (int)Math.Floor(height * videoRes);
                    left = (this.Width - width) / 2;
                }

                hr = this.videoWindow.SetWindowPosition(left, top, width, height);
            }
            else
            {
                this.Parent.MinimumSize = (this.Dock == DockStyle.Fill) ? new System.Drawing.Size(this.Resolution.Value.Width, this.Resolution.Value.Height) : new System.Drawing.Size(this.Resolution.Value.Width + 16, this.Resolution.Value.Height + 160);
                left = (this.Width - this.Resolution.Value.Width) / 2;
                top = (this.Height - this.Resolution.Value.Height) / 2;
                hr = this.videoWindow.SetWindowPosition(left, top, this.Resolution.Value.Width, this.Resolution.Value.Height);
            }

            DsError.ThrowExceptionForHR(hr);
        }

        public void AspectRatio(double aspectRatio)
        {
            if (this.Resolution == null) return;

            //int hr = 0;
            double nativeRes = (double)this.Width / (double)this.Height;

            if (aspectRatio < (double)nativeRes)
            {
                int width = (int)((double)this.Resolution.Value.Height * aspectRatio);
                //this.Width = width;
                //hr = this.basicVideo.put_DestinationWidth(width);

                this.Resolution = new Size(width, this.Height);
            }
            else
            {
                int height = (int)((double)this.Resolution.Value.Width * aspectRatio);
                //this.Height = height;
                //hr = this.basicVideo.put_DestinationHeight(height);

                this.Resolution = new Size(this.Width, height);
            }
            //DsError.ThrowExceptionForHR(hr);

            ScaleVideoFit();
        }

        private void MoveVideoWindow()
        {
            int hr = 0;

            // Track the movement of the container window and resize as needed
            if (this.videoWindow != null)
            {
                hr = this.videoWindow.SetWindowPosition(this.ClientRectangle.Left, this.ClientRectangle.Top, this.ClientRectangle.Width, this.ClientRectangle.Height);
                DsError.ThrowExceptionForHR(hr);
            }
        }

        public void ToggleMute()
        {
            int hr = 0;

            if ((this.graphBuilder == null) || (this.basicAudio == null)) return;

            // Read current volume
            hr = this.basicAudio.get_Volume(out this.currentVolume);

            if (hr == -1) //E_NOTIMPL
            {
                // Fail quietly if this is a video-only media file
                return;
            }
            else if (hr < 0)
            {
                return;
            }

            // Switch volume levels
            this.Mute = !this.Mute;
            if (this.Mute)
            {
                this.currentVolume = VOLUME_MIN;
            }
            else
            {
                this.currentVolume = convertVolumeDb(this.Volume);
            }

            // Set new volume
            hr = this.basicAudio.put_Volume(this.currentVolume);
            DsError.ThrowExceptionForHR(hr);
        }

        public int ToggleFullScreen()
        {
            int hr = 0;
            OABool lMode;

            // Don't bother with full-screen for audio-only files
            if ((this.MediaType == MediaType.Audio) || (this.videoWindow == null)) return 0;

            // Read current state
            hr = this.videoWindow.get_FullScreenMode(out lMode);
            DsError.ThrowExceptionForHR(hr);

            if (lMode == OABool.False)
            {
                // Save current message drain
                hr = this.videoWindow.get_MessageDrain(out drain);
                DsError.ThrowExceptionForHR(hr);

                // Set message drain to application main window
                hr = this.videoWindow.put_MessageDrain(this.Handle);
                DsError.ThrowExceptionForHR(hr);

                // Switch to full-screen mode
                lMode = OABool.True;
                hr = this.videoWindow.put_FullScreenMode(lMode);
                DsError.ThrowExceptionForHR(hr);

                this.FullScreen = true;
            }
            else
            {
                // Switch back to windowed mode
                lMode = OABool.False;
                hr = this.videoWindow.put_FullScreenMode(lMode);
                DsError.ThrowExceptionForHR(hr);

                // Undo change of message drain
                hr = this.videoWindow.put_MessageDrain(drain);
                DsError.ThrowExceptionForHR(hr);

                // Reset video window
                hr = this.videoWindow.SetWindowForeground(OABool.True);
                DsError.ThrowExceptionForHR(hr);

                this.FullScreen = false;
            }
            return hr;
        }

        private void VideoPlayback_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            ToggleFullScreen();
        }

        private void VideoPlayback_Resize(object sender, EventArgs e)
        {
            ScaleVideoFit();
        }

        public void SetPlaybackPosition(double time)
        {
            int hr = this.mediaPosition.put_CurrentPosition(time);
            DsError.ThrowExceptionForHR(hr);
        }

        public void SetPlaybackRate(double rate)
        {
            this.mediaPosition.put_Rate(rate);
            this.PlaybackRate = rate;
        }

        public void SetVolume(int volume)
        {
            this.Volume = volume;
            this.currentVolume = convertVolumeDb(volume);

            int hr = this.basicAudio.put_Volume(this.currentVolume);
            DsError.ThrowExceptionForHR(hr);
        }

        public int convertVolumeDb(int percent)
        {
            double db = -100;
            if (percent > 0)
            {
                double pct = (double)percent / 100;
                double log = Math.Log10(pct);
                db = 20 * log;
            }

            return (int)(db * 100);
        }
    }
}
